﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SmartMathLibrary.DataMining.ClusterAnalysis
{
    /// <summary>
    /// This class provides the creation of a self organising map with neural network support.
    /// </summary>
    [Serializable]
    public class SelfOrganisingMap
    {
        /// <summary>
        /// This field holds the collection of weights.
        /// </summary>
        private SelfOrganisingMapNeuron[,] outputs;

        /// <summary>
        /// This field holds the current iteration.
        /// </summary>
        private int iteration;

        /// <summary>
        /// This field holds the side length of output grid.
        /// </summary>
        private readonly int length;

        /// <summary>
        /// This field holds the number of input dimensions.
        /// </summary>
        private readonly int dimensions;

        /// <summary>
        /// This object generates random numbers which will be used by the algorithm.
        /// </summary>
        private readonly Random random;

        /// <summary>
        ///  This field holds the object references.
        /// </summary>
        private readonly List<Object> labels;

        /// <summary>
        /// This field holds the pattern of the map.
        /// </summary>
        private readonly List<double[]> patterns;

        /// <summary>
        /// This field holds the result of the learning.
        /// </summary>
        private readonly List<SelfOrganisingMapResultItem> resultItems;

        /// <summary>
        /// Initializes a new instance of the <see cref="SelfOrganisingMap"/> class.
        /// </summary>
        /// <param name="length">The number of items of the map.</param>
        /// <param name="dimensions">The dimensions of the map.</param>
        public SelfOrganisingMap(int length, int dimensions)
        {
            this.random = new Random();
            this.length = length;
            this.dimensions = dimensions;
            this.labels = new List<Object>();
            this.patterns = new List<double[]>();
            this.resultItems = new List<SelfOrganisingMapResultItem>();

            this.Initialise();
        }

        /// <summary>
        /// Gets the result items of the learning progress.
        /// </summary>
        /// <value>The result items of the learning progress.</value>
        public List<SelfOrganisingMapResultItem> ResultItems
        {
            get { return resultItems; }
        }

        /// <summary>
        /// Initialises the internal map.
        /// </summary>
        private void Initialise()
        {
            outputs = new SelfOrganisingMapNeuron[length,length];

            for (int i = 0; i < length; i++)
            {
                for (int j = 0; j < length; j++)
                {
                    outputs[i, j] = new SelfOrganisingMapNeuron(i, j, length);
                    outputs[i, j].Weights = new double[dimensions];

                    for (int k = 0; k < dimensions; k++)
                    {
                        outputs[i, j].Weights[k] = random.NextDouble();
                    }
                }
            }
        }

        /// <summary>
        /// Adds an item to the map. The maximum items to add is defined by the lenght parameter of the 
        /// constructor.
        /// </summary>
        /// <param name="item">The item to add.</param>
        public void AddItem(SelfOrganisingMapItem item)
        {
            if (item == (SelfOrganisingMapItem) null)
            {
                throw new ArgumentNullException("item");
            }

            if (this.dimensions != item.Data.Length)
            {
                throw new ArgumentException("The dimension of the map have to be even with the dimension of the item.");
            }

            if (this.length < (this.patterns.Count + 1))
            {
                throw new ArgumentException("The maximum numbers of items is reached.");
            }

            this.labels.Add(item.ReferenceObject);
            this.patterns.Add(item.Data);
        }

        /// <summary>
        /// Executes the training.
        /// </summary>
        public void ExecuteTraining()
        {
            this.NormalisePatterns();
            this.Train(1e-7);
            this.ExportItems();
        }

        /// <summary>
        /// Executes the training.
        /// </summary>
        /// <param name="precision">The precision of the result.</param>
        public void ExecuteTraining(double precision)
        {
            this.NormalisePatterns();
            this.Train(precision);
            this.ExportItems();
        }

        /// <summary>
        /// Normalises the patterns.
        /// </summary>
        private void NormalisePatterns()
        {
            for (int j = 0; j < dimensions; j++)
            {
                double sum = 0;

                for (int i = 0; i < patterns.Count; i++)
                {
                    sum += patterns[i][j];
                }

                double average = sum / patterns.Count;

                for (int i = 0; i < patterns.Count; i++)
                {
                    patterns[i][j] = patterns[i][j] / average;
                }
            }
        }

        /// <summary>
        /// Starts the training.
        /// </summary>
        /// <param name="maxError">The maximum error.</param>
        private void Train(double maxError)
        {
            double currentError = double.MaxValue;

            while (currentError > maxError)
            {
                currentError = 0;

                List<double[]> trainingSet = new List<double[]>();

                foreach (double[] pattern in patterns)
                {
                    trainingSet.Add(pattern);
                }

                for (int i = 0; i < patterns.Count; i++)
                {
                    double[] pattern = trainingSet[random.Next(patterns.Count - i)];

                    currentError += this.TrainPattern(pattern);
                    trainingSet.Remove(pattern);
                }

                Console.WriteLine(currentError.ToString("0.0000000"));
            }
        }

        /// <summary>
        /// Trains the specified pattern.
        /// </summary>
        /// <param name="pattern">The pattern to train.</param>
        /// <returns>The error.</returns>
        private double TrainPattern(double[] pattern)
        {
            double error = 0;
            SelfOrganisingMapNeuron winner = this.IntermitWinner(pattern);

            for (int i = 0; i < length; i++)
            {
                for (int j = 0; j < length; j++)
                {
                    error += outputs[i, j].UpdateWeights(pattern, winner, iteration);
                }
            }

            iteration++;

            return Math.Abs(error / (length * length));
        }

        /// <summary>
        /// Exports the items.
        /// </summary>
        private void ExportItems()
        {
            this.resultItems.Clear();

            for (int i = 0; i < patterns.Count; i++)
            {
                SelfOrganisingMapNeuron n = this.IntermitWinner(patterns[i]);

                this.resultItems.Add(new SelfOrganisingMapResultItem(n.X, n.Y, labels[i]));
            }
        }

        /// <summary>
        /// Intermits the winner.
        /// </summary>
        /// <param name="pattern">The pattern.</param>
        /// <returns>The winner.</returns>
        private SelfOrganisingMapNeuron IntermitWinner(double[] pattern)
        {
            SelfOrganisingMapNeuron winner = null;
            double min = double.MaxValue;

            for (int i = 0; i < length; i++)
            {
                for (int j = 0; j < length; j++)
                {
                    double d = ComputeDistance(pattern, outputs[i, j].Weights);

                    if (d < min)
                    {
                        min = d;
                        winner = outputs[i, j];
                    }
                }
            }

            return winner;
        }

        /// <summary>
        /// Computes the distance.
        /// </summary>
        /// <param name="vector1">The first vector.</param>
        /// <param name="vector2">The second vector.</param>
        /// <returns>The computed distance.</returns>
        private static double ComputeDistance(double[] vector1, double[] vector2)
        {
            double value = 0;

            for (int i = 0; i < vector1.Length; i++)
            {
                value += Math.Pow((vector1[i] - vector2[i]), 2);
            }

            return Math.Sqrt(value);
        }
    }
}